/*
   Bacula(R) - The Network Backup Solution

   Copyright (C) 2000-2025 Kern Sibbald

   The original author of Bacula is Kern Sibbald, with contributions
   from many others, a complete list can be found in the file AUTHORS.

   You may use this file and others of this release according to the
   license defined in the LICENSE file, which includes the Affero General
   Public License, v3.0 ("AGPLv3") and some additional permissions and
   terms pursuant to its AGPLv3 Section 7.

   This notice must be preserved when any source code is
   conveyed and/or propagated.

   Bacula(R) is a registered trademark of Kern Sibbald.
*/

/*
   Written by Eric Bollengier, 2015

           Documentation about snapshot backend
----------------------------------------------------------------

The snapshot manager is using environment variables to communicate.

Variables:

SNAPSHOT_ACTION
  Operation such as:
     create, delete, list, mount, unmount, check, support, subvolume

SNAPSHOT_VOLUME
  Volume name
   ex: /dev/vgroot/home_Test-2014-01-01_00_00  (lvm)
       /home/.snapshots/Test-2014-01-01_00_00  (btrfs)
       /.zfs/snapshot/Test-2014-01-01_00_00    (zfs)

  The volume name is generated by the create command

SNAPSHOT_DEVICE
  Device name
   ex: /dev/vgroot/home (lvm)
       /home            (btrfs)
       /                (zfs)

  The device name can be found via getmntent()

SNAPSHOT_NAME
  Snapshot name, usually the Job name

SNAPSHOT_FSTYPE
  Device filesystem type, can be found via getmntent()
  ex: btrfs, zfs, ext4

SNAPSHOT_TYPE
  Snapshot backend type, generated by support command
  ex: lvm, btrfs, zfs

SNAPSHOT_MOUNTPOINT
  Device mount point, found via getmntent()

SNAPSHOT_SNAPMOUNTPOINT
  Snapshot mount point is generated by the mount command


                   Protocol
----------------------------------------------------------------

OK: exit code 0 and status=1 in the output
ERR: exit code <> 0 and/or status=0 in the output

status=1 keyword="value" keyword2="value2"

status=0 error="Error message"


                   Workflow
----------------------------------------------------------------

1) List filesystems
 get SNAPSHOT_DEVICE, SNAPSHOT_FSTYPE, SNAPSHOT_MOUNTPOINT

 volume="" name="" device="" createtime=""

2) Test if a filesystem supports snapshot feature
 SNAPSHOT_ACTION=support
 SNAPSHOT_DEVICE=/home
 SNAPSHOT_MOUNTPOINT=/home
 SNAPSHOT_FSTYPE=btrfs

 => status=1 type=btrfs device=/home
 => status=0

2a) Test if a filesystem contains subvolumes
 SNAPSHOT_ACTION=subvolumes
 SNAPSHOT_DEVICE=/home
 SNAPSHOT_FSTYPE=btrfs

 => dev=10 mountpoint=/home/subvol fstype=btrfs

3) Create a snapshot
 SNAPSHOT_ACTION=create
 SNAPSHOT_NAME=Test-2014-01-01_00_00 
 SNAPSHOT_DEVICE=/home
 SNAPSHOT_MOUNTPOINT=/home
 SNAPSHOT_FSTYPE=btrfs
 SNAPSHOT_TYPE=btrfs

 => status=1 volume="/home/.snapshots/Test-2014-01-01_00_00" createtdate=1418935776 type=btrfs

4) Mount the snapshot
 SNAPSHOT_ACTION=mount
 SNAPSHOT_NAME=Test-2014-01-01_00_00 
 SNAPSHOT_DEVICE=/home
 SNAPSHOT_MOUNTPOINT=/home
 SNAPSHOT_FSTYPE=btrfs
 SNAPSHOT_TYPE=btrfs

 => status=1 volume="/home/.snapshots/Test-2014-01-01_00_00" createtdate=1418935776 type=btrfs
 

5) Unmount the snapshot
 SNAPSHOT_ACTION=unmount
 SNAPSHOT_SNAPMOUNTPOINT=/home/.snapshots/Test-2014-01-01_00_00

 => status=1

6) Delete the snapshot
 SNAPSHOT_ACTION=delete
 SNAPSHOT_VOLUME=/home/.snapshot/Test-2014-01-01_00_00

 => status=1

 */

#include "bacula.h"
#include "filed.h"
#define USE_CMD_PARSER
#include "plugins/fd/fd_common.h"
#undef Jmsg
#include "fd_snapshot.h"
#define APPMANAGER_CMD "%eappmanager"
#define APP_DIR "/tmp/app.d"
#define SNAPSHOT_CMD "%ebsnapshot"

/* Defined in messages.c */
extern char *exepath;

/* Catalog interface with the director */
static char CreateSnap[] = "CatReq Job=%s new_snapshot name=%s volume=%s device=%s tdate=%d type=%s retention=%s";
static char DelSnap[] = "CatReq Job=%s del_snapshot name=%s device=%s";
static char GetSnap[] = "CatReq Job=%s get_snapshot name=%s volume=%s";

/* Command line interface with the director */
static char LsCmd[] = "snapshot ls name=%127s volume=%s device=%s tdate=%d type=%127s path=%s";
static char DelCmd[] = "snapshot del name=%127s volume=%s device=%s tdate=%d type=%127s";
static char QueryCmd[] = "snapshot query name=%127s volume=%s device=%s tdate=%d type=%127s";
static char PruneCmd[] = "snapshot prune volume=%s type=%127s";
static char SyncCmd[] = "snapshot sync volume=%s type=%127%";
static char ListCmd[] = "snapshot list";
static char ConfCmd[] = "snapshot retention=%50s";

/* Small function to quickly tell us if we can do snapshot here */
static bool is_snapshot_supported(JCR *jcr)
{
   bool ret;
   struct stat sp;
   POOLMEM    *cmd = get_pool_memory(PM_FNAME);
   const char *p;
   const char *str;
   char add[20];

   /* We are not really interested by arguments, just
    * the filename
    */
   *cmd = 0;
   for (p=me->snapshot_command; *p; p++) {
      if (*p == '%') {
         switch (*++p) {
         case '%':
            str = "%";
            break;
         case 'e':
            str = NPRTB(exepath);
            break;
         default:
            add[0] = '%';
            add[1] = *p;
            add[2] = 0;
            str = add;
         }

      } else if (*p == ' ') {
         break;

      } else {
         add[0] = *p;
         add[1] = 0;
         str = add;
      }
      pm_strcat(cmd, str);
   }

   ret = stat(cmd, &sp) == 0;
   free_pool_memory(cmd);
   Dmsg1(10, "Snapshot = %d\n", ret);
   return ret;
}

/* Return the default snapshot handler, must be freed at the end */
char *snapshot_get_command()
{
   return bstrdup(SNAPSHOT_CMD);
}

/* Initialize the snapshot manager at the begining of the
 * job and create snapshots
 */
bool open_snapshot_backup_session(JCR *jcr)
{
   if (!is_snapshot_supported(jcr)) {
      Dmsg0(DT_SNAPSHOT, "Snapshot not supported\n");
      return false;
   }

   jcr->snap_mgr = New(snapshot_manager(jcr));
   /* Get all volumes and subvolumes */
   if (!jcr->snap_mgr->scan_mtab()) {
      berrno be;                /* error probably in errno */
      Dmsg1(DT_SNAPSHOT, "Unable to scan mtab. ERR=%s\n", be.bstrerror());
      Jmsg(jcr, M_ERROR, 0, "Unable to scan mtab to determine devices to snapshot\n");
      return false;
   }
   /* Match the volume list with the fileset */
   if (!jcr->snap_mgr->scan_fileset()) {
      Jmsg(jcr, M_ERROR,0, "Unable to scan fileset to determine devices to snapshot\n");
      return false;
   }
   /* Create fileset needed */
   if (!jcr->snap_mgr->create_snapshots()) {
      /* Error message already displayed if needed */
      return false;
   }
   return true;                 /* We should have some snapshots */
}

/* Command that can be called from outside */
bool list_all_snapshots(JCR *jcr, alist *lst)
{
   snapshot_manager snap_mgr(jcr);

   /* Get all volumes and subvolumes */
   if (!snap_mgr.scan_mtab()) {
      return false;
   }
   /* list snapshots */
   if (snap_mgr.list_snapshots(lst)) {
      return false;
   }
   return true;
}

/* Cleanup the snapshot manager at the end of the job */
void close_snapshot_backup_session(JCR *jcr)
{
   if (jcr->snap_mgr) {
      jcr->snap_mgr->cleanup_snapshots();
      delete jcr->snap_mgr;
      jcr->snap_mgr = NULL;
   }
}

class snapshot;

/* Device that exists on the system */
class fs_device: public SMARTALLOC
{
public:
   rblink link;

   uint32_t  dev;               /* dev no */
   char     *mountpoint;        /* where it's mounted */
   char     *fstype;            /* ntfs, ext3, ext4... */
   char     *device;            /* /dev/mapper/xxx */

   bool      supportSnapshotTested; /* True if support() was called */
   bool      isSuitableForSnapshot; /* Compatible with snapshots */
   bool      inSnapshotSet;
   bool      inFileSet;
   snapshot *snap;              /* Associated snapshot */

   dlist     *include;          /* Where the fs_device was found in the fileset */
   void      *node;             /* At which node */


   fs_device(): 
      dev(0), mountpoint(NULL), fstype(NULL), device(NULL), 
         supportSnapshotTested(false), isSuitableForSnapshot(false), snap(NULL)
      {
      };

   fs_device(uint32_t adev, const char *adevice, const char *amountpoint, const char *aftype) {
      dev        = adev;
      fstype     = bstrdup(aftype);
      device    = bstrdup(adevice);
      mountpoint = bstrdup(amountpoint);
      supportSnapshotTested = false;
      isSuitableForSnapshot = false;
      inSnapshotSet = false;
      inFileSet = false;
      snap       = NULL;
      include    = NULL;
      node       = NULL;
   };

   ~fs_device() {
      destroy();
   };

   /* Call support() and cache the result in supportSnapshotTested and isSuitableForSnapshot */
   bool can_do_snapshot();

   void setInFileSet(dlist *inc, void *where) {
      include = inc;            /* where we need to include subvolumes */
      node = where;             /* after which node */
      inFileSet = true;
      inSnapshotSet = true;
   };

   void set_snap(snapshot *s) {
      snap = s;
   };

   void destroy();
};

/* The device list is stored in a rblist, using the
 * dev no as key. The devno can be found in every stat()
 * packet.
 */
static int compare_entries(void *item1, void *item2)
{
   fs_device *dev1 = (fs_device *) item1;
   fs_device *dev2 = (fs_device *) item2;
   if (dev1->dev > dev2->dev) {
      return 1;

   } else if (dev1->dev < dev2->dev) {
      return -1;

   } else {
      return 0;
   }
}

static int search_entry(void *item1, void *item2)
{
   uint32_t dev1 = (intptr_t) item1;
   fs_device* dev2 = (fs_device *) item2;
   if (dev1 > dev2->dev) {
      return 1;

   } else if (dev1 < dev2->dev) {
      return -1;

   } else {
      return 0;
   }
}

/* List of all fd_device that are on the system
 * Some devices are excluded automatically from
 * the list, such as proc, sysfs, etc...
 */
class mtab: public SMARTALLOC
{
public:
   rblist     *entries;
   int         sCount;          /* Snapshot count */
   int         dCount;          /* Device count */
   mtab() {
      fs_device *elt = NULL;
      entries = New(rblist(elt, &elt->link));
      dCount = sCount = 0;
   };

   ~mtab() {
      fs_device *elt;
      foreach_rblist(elt, entries) {
         elt->destroy();
      }
      delete entries;
   };

   /* Have we devices for snapshot in our list ? */
   bool empty() {
      return sCount == 0;
   };

   /* Get a fs_device corresponding to a file */
   fs_device *search(char *file);

   /* Get subvolumes for a specific device */
   bool get_subvolumes(uint32_t dev, alist *items, FF_PKT *ff) {
      int l, l2;
      fs_device *elt, *elt2;
      elt = (fs_device *)entries->search((void*)(intptr_t)dev, search_entry);
      if (!elt) {
         return false;
      }

      foreach_rblist(elt2, entries) {
         if (elt2->dev == elt->dev) {
            continue;
         }
         l = strlen(elt->mountpoint);
         if (strncmp(elt2->mountpoint, elt->mountpoint, l) == 0) {
            l2 = strlen(elt2->mountpoint);
            if (l2 > l && !IsPathSeparator(elt2->mountpoint[l])) {
               Dmsg1(DT_SNAPSHOT|50, "Not subvolume %s\n", elt2->mountpoint);

            } else if (file_is_excluded(ff, elt2->mountpoint)) {
            /* the mount point is included in the volume */
               Dmsg1(DT_SNAPSHOT|50, "Looks to be excluded %s\n", elt2->mountpoint);

            } else {
               items->append(elt2);
            }
         }
      }
      return items->size() > 0;
   };

   bool add_in_snapshot_set(char *file, dlist *inc, void *node) {

      fs_device *elt = search(file);
      if (!elt) {
         Dmsg1(DT_SNAPSHOT, "%s will not be added to snapshot set\n", file);
         return sCount == dCount;        /* not found in our list, skip it */
      }
      return add_in_snapshot_set(elt, inc, node);
   };

   bool add_in_snapshot_set(fs_device *elt, dlist *inc, void *node) {
      Dmsg4(DT_SNAPSHOT|10, "%s in=%d can=%d tested=%d\n", elt->mountpoint, elt->inSnapshotSet, 
            elt->isSuitableForSnapshot, elt->supportSnapshotTested);
      if (!elt->inSnapshotSet && elt->can_do_snapshot()) {
         Dmsg1(DT_SNAPSHOT, "Marking %s for snapshot\n", elt->mountpoint);
         elt->setInFileSet(inc, node);
         sCount++;
      }
      /* It will help to count when all devices are in the snapshot set */
      Dmsg2(DT_SNAPSHOT|10, "sCount %d = dCount %d\n", sCount, dCount); 
      return sCount == dCount;
   };

   bool add_entry(fs_device *vol) {
      fs_device *ret = (fs_device *) entries->insert(vol, compare_entries);
      if (ret == vol && vol->snap) {
         dCount++;              /* We skip directly FS such as /proc, /sys or /dev */
      }
      return ret == vol;
   };
};

/* Snapshot descriptor, used to communicate with the snapshot
 * backend on the system.
 */
class snapshot: public SMARTALLOC
{
private:
   JCR *jcr;

public:
   const char *action;             /* current action */
   char     Name[MAX_NAME_LENGTH]; /* Name of the snapshot */
   char     Type[MAX_NAME_LENGTH]; /* lvm, btrfs, netapp */
   char     FSType[MAX_NAME_LENGTH];     /* btrfs, zfs, ext3 */
   char     CreateDate[MAX_TIME_LENGTH]; /* Creation date */
   time_t   CreateTDate;                 /* Creation date in seconds */
   int64_t  size;               /* Size of the snapshot */
   int      status;             /* snapshot status */
   utime_t  Retention;          /* Snapshot retention, might come from Pool/FileSet */

   POOLMEM *Volume;             /* Path of the volume */
   POOLMEM *Device;             /* Device path */
   POOLMEM *MountPoint;         /* Device Mount point */
   POOLMEM *SnapMountPoint;     /* Snapshot Mount point */
   POOLMEM *path;               /* path used in ls query */
   POOLMEM *errmsg;             /* Error message generated by commands */
   POOLMEM *SnapDirectory;      /* Where snapshots are stored */

   char   **env;                /* Variables used to call snapshot */
   bool     mounted;            /* True if mounted on SnapMountPoint */
   bool     created;            /* True if the snapshot is created */

   snapshot(JCR *ajcr) {
      jcr    = ajcr;
      env    = NULL;
      path   = get_pool_memory(PM_FNAME);
      errmsg = get_pool_memory(PM_MESSAGE);
      Volume = get_pool_memory(PM_FNAME);
      Device = get_pool_memory(PM_FNAME);
      MountPoint     = get_pool_memory(PM_FNAME);
      SnapMountPoint = get_pool_memory(PM_FNAME);
      SnapDirectory  = get_pool_memory(PM_FNAME);
      reset();
   };

   ~snapshot() {
      free_pool_memory(path);
      free_pool_memory(errmsg);
      free_pool_memory(Volume);
      free_pool_memory(Device);
      free_pool_memory(MountPoint);
      free_pool_memory(SnapMountPoint);
      free_pool_memory(SnapDirectory);
      free_env();
   };

   void reset() {
      *SnapDirectory = *Type = *FSType = *SnapMountPoint = 0;
      *MountPoint = *Volume = *Device = *path = *errmsg = 0;
      action    = NULL;
      size      = -1;
      status    = 0;
      mounted   = false;
      created   = false;
      Retention = jcr->snapshot_retention;
   };

   /* Free the env[] structure */
   void free_env() {
      if (env) {
         for (int i=0; env[i] ; i++) {
            free(env[i]);
         }
         free(env);
         env = NULL;
      }
   };

   void set_device(const char *d) {
      pm_strcpy(Device, d);
   };

   void set_mountpoint(const char *d) {
      pm_strcpy(MountPoint, d);
   };

   void set_name(const char *n) {
      bstrncpy(Name, n, sizeof(Name));
   };

   void set_fstype(const char *n) {
      bstrncpy(FSType, n, sizeof(FSType));
   };

   void set_action(const char *a) {
      action = a;
   };

   /* Convert a real top path to a snapshot path
    * and set proper variables inside ff_pkt
    * to translate back all subfiles.
    */
   bool convert_path(FF_PKT *ff) {
      if (!*MountPoint || !*SnapMountPoint) {
         Dmsg2(DT_SNAPSHOT, "MountPoint=%s SnapMountPoint=%s\n", 
               NPRT(MountPoint), NPRT(SnapMountPoint));
         return false;
      }
      if (!ff->snap_top_fname) {
         ff->snap_top_fname = get_pool_memory(PM_FNAME);
      }
      ff->volume_path = MountPoint;       /* /tmp */
      ff->snapshot_path = SnapMountPoint; /* /tmp/.snapshot/Job.20140502.01.01.01 */
      ff->top_fname_save = ff->top_fname;

      int mp_first = strlen(MountPoint); /* will point to after MountPoint in top_fname */
      int last = pm_strcpy(ff->snap_top_fname, SnapMountPoint);
      last = MAX(last - 1, 0);

      /* We need to concat path and avoid double / and no / */
      if (ff->snap_top_fname[last] == '/') {
         if (ff->top_fname[mp_first] == '/') {
            ff->snap_top_fname[last] = 0; /* strip double / */
         }
      } else {                            /* no / at all */
         if (ff->top_fname[mp_first] != '/') {
            pm_strcat(ff->snap_top_fname, "/");
         }
      }

      pm_strcat(ff->snap_top_fname, ff->top_fname + mp_first);
      ff->top_fname = ff->snap_top_fname;
      Dmsg1(DT_SNAPSHOT|50, "top_fname=%s\n", ff->top_fname);
      return true;
   };

   /* Create a environment used in the child process */
   int edit_snapshot_env() {
      int      i   = 0;
      POOLMEM *tmp = get_pool_memory(PM_FNAME);
      free_env();

      /* Update "10" to add more variables */
      env = (char **) malloc(sizeof(char *) * 15);

      if (*Name) {
         Mmsg(tmp, "SNAPSHOT_NAME=%s", Name);
         env[i++] = bstrdup(tmp);
      }
      if (*Volume) {
         Mmsg(tmp, "SNAPSHOT_VOLUME=%s", Volume);
         env[i++] = bstrdup(tmp);
      }
      if (*Device) {
         Mmsg(tmp, "SNAPSHOT_DEVICE=%s", Device);
         env[i++] = bstrdup(tmp);
      }
      if (*Type) {
         Mmsg(tmp, "SNAPSHOT_TYPE=%s", Type);
         env[i++] = bstrdup(tmp);
      }
      if (*FSType) {
         Mmsg(tmp, "SNAPSHOT_FSTYPE=%s", FSType);
         env[i++] = bstrdup(tmp);
      }
      if (*MountPoint) {
         Mmsg(tmp, "SNAPSHOT_MOUNTPOINT=%s", MountPoint);
         env[i++] = bstrdup(tmp);
      }
      if (*SnapDirectory) {
         Mmsg(tmp, "SNAPSHOT_SNAPDIRECTORY=%s", SnapDirectory);
         env[i++] = bstrdup(tmp);
      }
      if (*SnapMountPoint) {
         Mmsg(tmp, "SNAPSHOT_SNAPMOUNTPOINT=%s", SnapMountPoint);
         env[i++] = bstrdup(tmp);
      }

      Mmsg(tmp, "SNAPSHOT_WORKING=%s", me->working_directory);
      env[i++] = bstrdup(tmp);

      /* When adding new entries, do not forget to add more slots to env[] */

      Mmsg(tmp, "SNAPSHOT_ACTION=%s", action);
      env[i++] = bstrdup(tmp);

      env[i] = NULL;            /* last record */

      if (chk_dbglvl(DT_SNAPSHOT|100)) {
         for (i = 0; env[i] ; i++) {
            Dmsg1(DT_SNAPSHOT|100, "%s\n", env[i]);
         }
      }

      free_pool_memory(tmp);
      return 1;
   };

   /* Edit the command line if needed */
   int edit_snapshot_codes(POOLMEM **omsg, const char *imsg) {
      const char *p;
      const char *str;
      char add[20];

      **omsg = 0;
      for (p=imsg; *p; p++) {
         if (*p == '%') {
            switch (*++p) {
            case '%':
               str = "%";
               break;
            case 'e':
               str = NPRTB(exepath);
               break;
            case 'n':
               str = Name;
               break;
            case 'v':
               str = Volume;
               break;
            case 'd':
               str = Device;
               break;
            case 'D':
               str = SnapDirectory;
               break;
            case 'a':
               str = NPRT(action);
               break;
            case 't':
               str = Type;
               break;
            case 'f':
               str = FSType;
               break;
            case 'p':
               str = MountPoint;
               break;
            case 's':
               str = SnapMountPoint;
               break;
            default:
               add[0] = '%';
               add[1] = *p;
               add[2] = 0;
               str = add;
            }

         } else {
            add[0] = *p;
            add[1] = 0;
            str = add;
         }
         pm_strcat(omsg, str);
      }

      if (chk_dbglvl(DT_SNAPSHOT|10)) {
         POOL_MEM tmp;
         Mmsg(tmp, " -d %d -o %s/bsnapshot.log ", debug_level, working_directory);
         pm_strcat(omsg, tmp.c_str());
      }

      Dmsg2(DT_SNAPSHOT|30, "edit_snapshot_codes: %s -> %s\n", imsg, *omsg);
      return 1;
   };

   /* Call the snapshot backend to know if we can snapshot the current FS */
   int support_snapshot(fs_device *vol) {
      arg_parser cmd;
      status = 0;

      reset();
      set_device(vol->device);
      set_mountpoint(vol->mountpoint);
      set_fstype(vol->fstype);

      if (!do_command("support", &cmd)) {
         goto bail_out;
      }
      scan_arg(&cmd);

   bail_out:
      Dmsg2(DT_SNAPSHOT|50, "%s snapshot support status=%d\n", vol->mountpoint, status);
      return status;
   };

   /* Scan sub volumes for a particular volume */
   int scan_subvolumes(fs_device *vol, alist *lst) {
      int ret = 0;
      arg_parser cmd;
      uint32_t   dev=0;
      char      *mp=NULL, *fstype=NULL, *device=Device;

      reset();
      set_device(vol->device);
      set_mountpoint(vol->mountpoint);
      set_fstype(vol->fstype);

      if (!do_command("subvolumes", &cmd)) {
         goto bail_out;
      }

      for (int i = 0; i < cmd.argc ; i++) {
         if (strcasecmp(cmd.argk[i], "Dev") == 0 && cmd.argv[i]) {
            dev = str_to_int64(cmd.argv[i]);

         } else if (strcasecmp(cmd.argk[i], "MountPoint") == 0 && cmd.argv[i]) {
            mp = cmd.argv[i];

         } else if (strcasecmp(cmd.argk[i], "Device") == 0 && cmd.argv[i]) {
            device = cmd.argv[i];

         } else if (strcasecmp(cmd.argk[i], "FSType") == 0 && cmd.argv[i]) {
            fstype = cmd.argv[i];
            if (mp && fstype && dev) {
               fs_device *elt = New(fs_device(dev, device, mp, fstype));
               lst->append(elt);
               /* reset variables */
            }
            dev = 0;
            mp  = fstype = NULL;
            device = Device;
         }
      }
      ret = 1;

   bail_out:
      return ret;
   };

   /* Prune current snapshots
    *  - List snapshots available on the director, keep a list locally
    *  - get mtab, list snapshots for all devices, or devices that are in the director list
    */
   int prune(BSOCK *bs) {
      return 1;
   };

   /* List local snapshots, list director snapshots, and synchronize the two */
   int sync(BSOCK *bs) {
      return 1;
   };

   /* List files from a snapshot
    *  Need to set the Volume and the Path
    */
   int ls(BSOCK *bs) {
      return 1;
   };

   /* Scan common arguments */
   int scan_arg(arg_parser *cmd) {
      for (int i = 0; i < cmd->argc ; i++) {
         if (strcasecmp(cmd->argk[i], "Volume") == 0 && cmd->argv[i]) {
            pm_strcpy(Volume, cmd->argv[i]);

         } else if (strcasecmp(cmd->argk[i], "CreateDate") == 0 && cmd->argv[i]) {
            bstrncpy(CreateDate, cmd->argv[i], sizeof(CreateDate));
            CreateTDate = str_to_utime(CreateDate);

         } else if (strcasecmp(cmd->argk[i], "CreateTDate") == 0 && cmd->argv[i]) {
            CreateTDate = str_to_int64(cmd->argv[i]);
            bstrftimes(CreateDate, sizeof(CreateDate), CreateTDate);

         } else if (strcasecmp(cmd->argk[i], "Type") == 0 && cmd->argv[i]) {
            bstrncpy(Type, cmd->argv[i], sizeof(Type));

         } else if (strcasecmp(cmd->argk[i], "SnapMountPoint") == 0 && cmd->argv[i]) {
            pm_strcpy(SnapMountPoint, cmd->argv[i]);

         } else if (strcasecmp(cmd->argk[i], "SnapDirectory") == 0 && cmd->argv[i]) {
            pm_strcpy(SnapDirectory, cmd->argv[i]);
            strip_trailing_slashes(SnapDirectory);

         } else if (strcasecmp(cmd->argk[i], "status") == 0 && cmd->argv[i]) {
            status = str_to_int64(cmd->argv[i]);

         } else if (strcasecmp(cmd->argk[i], "Device") == 0 && cmd->argv[i]) {
            pm_strcpy(Device, cmd->argv[i]);
         }
      }
      return 1;
   };

   /* Create a snapshot with already given attributes
    *  Need to set Name and Device at the minimum
    */
   int create() {
      int ret = 0;
      arg_parser cmd;

      if (!*Name || !*Device) {
         goto bail_out;
      }

      Dmsg2(DT_SNAPSHOT, "Create Snapshot of %s %s\n", Device, Name);

      /* TODO: see if we handle multiple snapshots per call */
      if (!do_command("create", &cmd)) {
         goto bail_out;
      }

      scan_arg(&cmd);
      created = 1;

      ret = 1;

   bail_out:
      return ret;
   };

   int mount() {
      arg_parser cmd;
      status = 0;

      if (!*Name || !*Volume || !*Device || !*Type) {
         goto bail_out;
      }

      Dmsg1(DT_SNAPSHOT, "Doing mount of %s\n", Volume);

      if (!do_command("mount", &cmd)) {
         goto bail_out;
      }

      *SnapMountPoint = 0;

      scan_arg(&cmd);

      mounted = (status > 0 && *SnapMountPoint);

   bail_out:
      return status;
   };

   int unmount() {
      arg_parser cmd;
      status = 0;

      if (!*Name || !*SnapMountPoint || !*Type || !mounted) {
         goto bail_out;
      }

      Dmsg1(DT_SNAPSHOT, "Doing unmount of a %s\n", SnapMountPoint);

      if (!do_command("unmount", &cmd)) {
         goto bail_out;
      }

      scan_arg(&cmd);

      mounted = 0;

   bail_out:
      return status;
   };

   /* Delete a snapshot with the given device name */
   int del() {
      int ret = 0;
      arg_parser cmd;
      if (!*Name || !*Volume) {
         goto bail_out;
      }
      ret = do_command("delete", &cmd);
   bail_out:
      return ret;
   };

   /* TODO: Need to read stdout as well */
   int do_command(const char *act, arg_parser *cmd) {
      int ret = 0, rcode;
      POOLMEM *command = get_pool_memory(PM_FNAME);
      POOLMEM *out = get_pool_memory(PM_FNAME);

      set_action(act);
      edit_snapshot_codes(&command, me->snapshot_command);
      edit_snapshot_env();

      Dmsg1(DT_SNAPSHOT|20, "Execute %s command\n", act);

      if (*command == 0) {
         Mmsg(errmsg, _("Error while creating command string %s.\n"), act);
         Dmsg1(DT_SNAPSHOT, "%s", errmsg);
         goto bail_out;
      }

      /* If the exit code is 1, we can expect to have a clear error
       * message in the output
       */
      if ((rcode = run_program_full_output(command, 600, out, env)) != 0) {
         if ((rcode & ~b_errno_exit) == 1) { /* exit 1, look the output */
            if (cmd->parse_cmd(out) == bRC_OK) {
               int i = cmd->find_arg_with_value("status");
               if (i >= 0) {
                  /* If we have a status, take it */
                  status = str_to_int64(cmd->argv[i]);
               } else {
                  status = 0;
               }
               i = cmd->find_arg_with_value("error");
               if (i >= 0) {
                  pm_strcpy(errmsg, cmd->argv[i]);
                  Dmsg1(DT_SNAPSHOT|20, "%s\n", errmsg);
                  goto bail_out;
               }
            }
         }
         berrno be;
         Mmsg(errmsg, _("Error while executing \"%s\" %s. %s %s\n"), 
              act, command, out, be.bstrerror());
         Dmsg1(DT_SNAPSHOT, "%s", errmsg);
         goto bail_out;
      }

      /* Need to decode the output of the script
       * TODO: some commands will have multiple lines
       */
      if (cmd->parse_cmd(out) != bRC_OK) {
         Dmsg2(DT_SNAPSHOT, "snapshot command %s output error: [%s]\n", act, out);
         Mmsg(errmsg, _("Unable to parse snapshot command output\n"));
         goto bail_out;
      }
      Dmsg1(DT_SNAPSHOT|50, "ret = %s\n", out);
      ret = 1;
   bail_out:
      free_pool_memory(command);
      free_pool_memory(out);
      return ret;
   };

   int list(alist *lst) {
      Dmsg0(DT_SNAPSHOT, "Doing list of a snapshots of a given device\n");
      snapshot  *snap;
      arg_parser cmd;
      int i, ret=0, status=1;
      char *volume=NULL, *name=NULL, *device=NULL, *createdate=NULL, *error=NULL;
      utime_t createtdate = 0;

      /* TODO: Here we need to loop over a list */
      if (!do_command("list", &cmd)) {
         goto bail_out;
      }
      ret = 1;

      /* should get
       * volume=xxx device=zzzz name=yyy createtdate=12121212 size=xx status=xx error=xx type=lvm
       */
      for (i = 0; i < cmd.argc ; i++) {
         if (strcasecmp(cmd.argk[i], "Volume") == 0) {
            volume = cmd.argv[i];

         } else if (strcasecmp(cmd.argk[i], "Name") == 0) {
            name = cmd.argv[i];

         } else if (strcasecmp(cmd.argk[i], "Device") == 0) {
            device = cmd.argv[i];

         } else if (strcasecmp(cmd.argk[i], "Error") == 0) {
            error = cmd.argv[i];

         } else if (strcasecmp(cmd.argk[i], "Status") == 0) {
            status = str_to_int64(cmd.argv[i]);

         } else if (strcasecmp(cmd.argk[i], "Type") == 0) {
            snap = New(snapshot(jcr));
            pm_strcpy(snap->Volume, volume);
            pm_strcpy(snap->Device, NPRTB(device));
            bstrncpy(snap->Name, NPRTB(name), sizeof(snap->Name));
            bstrncpy(snap->Type, cmd.argv[i], sizeof(snap->Type));
            bstrncpy(snap->CreateDate, NPRTB(createdate), sizeof(snap->CreateDate));
            pm_strcpy(snap->errmsg, NPRTB(error));
            snap->status = status;
            snap->CreateTDate = createtdate;
            error = createdate = device = name = volume = NULL;
            status = 1;
            createtdate = 0;
            lst->append(snap);

         } else if (strcasecmp(cmd.argk[i], "CreateTDate") == 0) {
            createtdate = str_to_int64(cmd.argv[i]);

         } else if (strcasecmp(cmd.argk[i], "CreateDate") == 0) {
            createdate = cmd.argv[i];
            createtdate = str_to_utime(cmd.argv[i]);
         }
      }
   bail_out:
      return ret;
   };

   /* Query information about snapshot */
   int query() {
      Dmsg0(0, "Doing query of a snapshot\n");
      arg_parser cmd;
      int i, ret=0;

      if (!*Volume) {
         goto bail_out;
      }

      if (!do_command("query", &cmd)) {
         goto bail_out;
      }

      if ((i = cmd.find_arg_with_value("size")) >= 0) {
         size = str_to_int64(cmd.argv[i]);
      }

      if ((i = cmd.find_arg_with_value("status")) >= 0) {
         status = str_to_int64(cmd.argv[i]);
      }

      ret = 1;

   bail_out:
      return ret;
   };

   /* Quickly unbash all attributes after a sscanf */
   void unbash_spaces() {
      ::unbash_spaces(Volume);
      ::unbash_spaces(Device);
      ::unbash_spaces(path);
      ::unbash_spaces(Name);
      ::unbash_spaces(CreateDate);
      ::unbash_spaces(errmsg);
   };

   void bash_spaces() {
      ::bash_spaces(Volume);
      ::bash_spaces(Device);
      ::bash_spaces(path);
      ::bash_spaces(Name);
      ::bash_spaces(CreateDate);
      ::bash_spaces(errmsg);
   };

   /* Quicky make sure we have enough space to handle the request */
   void check_buffer_size(int len) {
      Volume = check_pool_memory_size(Volume, len);
      Device = check_pool_memory_size(Device, len);
      path   = check_pool_memory_size(path, len);
   };

   /* Create Catalog entry for the current snapshot */
   int create_catalog_entry() {
      int ret = 0;
      char ed1[50];
      bash_spaces();
      jcr->dir_bsock->fsend(CreateSnap,
                            jcr->Job, Name, Volume,
                            Device, CreateTDate, Type, edit_uint64(Retention, ed1));
      if (jcr->dir_bsock->recv() < 0) {
         Mmsg(errmsg, _("Unable to create snapshot record. ERR=%s\n"),
              jcr->dir_bsock->bstrerror());

      } else if (strncmp(jcr->dir_bsock->msg, "1000", 4) != 0) {
         Mmsg(errmsg, _("Unable to create snapshot record, got %s\n"),
              jcr->dir_bsock->msg);

      } else {
         ret = 1;               /* OK */
      }
      unbash_spaces();
      return ret;
   };

   /* Delete Catalog entry of the current snapshot */
   int delete_catalog_entry() {
      int ret = 0;
      bash_spaces();
      jcr->dir_bsock->fsend(DelSnap, jcr->Job, Name, Device);

      if (jcr->dir_bsock->recv() < 0) {
         Mmsg(errmsg, _("Unable to delete snapshot record. ERR=%s\n"),
              jcr->dir_bsock->bstrerror());

      } else if (strncmp(jcr->dir_bsock->msg, "1000", 4) != 0) {
         Mmsg(errmsg, _("Unable to delete snapshot record, got %s\n"),
              jcr->dir_bsock->msg);

      } else {
         ret = 1;               /* OK */
      }

      unbash_spaces();
      return ret;
   };

   /* Get Catalog entry of the current snapshot */
   int get_catalog_entry() {
      int ret = 0;
      arg_parser cmd;

      if (!*Name || !*Volume) {
         return ret;
      }

      bash_spaces();
      jcr->dir_bsock->fsend(GetSnap, jcr->Job, Name, Volume);

      if (jcr->dir_bsock->recv() < 0) {
         Mmsg(errmsg, _("Unable to get snapshot record. ERR=%s\n"),
              jcr->dir_bsock->bstrerror());

      } else if (strncmp(jcr->dir_bsock->msg, "1000", 4) != 0) {
         Mmsg(errmsg, _("Unable to get snapshot record, got %s\n"),
              jcr->dir_bsock->msg);

      } else {
         if (cmd.parse_cmd(jcr->dir_bsock->msg) != bRC_OK) {
            Mmsg(errmsg, _("Unable to parse command output\n"));
            scan_arg(&cmd);     /* Fill all parameters from director */

         } else {
            ret = 1;               /* OK */
         }
      }

      unbash_spaces();
      return ret;
   };

};

/* Should be after snapshot declaration */
void fs_device::destroy() {
   if (fstype) {
      free(fstype);
      fstype = NULL;
   }
   if (mountpoint) {
      free(mountpoint);
      mountpoint = NULL;
   }
   if (device) {
      free(device);
      device = NULL;
   }
   if (snap) {
      delete snap;
      snap = NULL;
   }
}

bool fs_device::can_do_snapshot() {
   if (snap && !supportSnapshotTested) {
      if (snap->support_snapshot(this)) {
         Dmsg2(DT_SNAPSHOT, "%s suitable for snapshot, type %s\n",
               mountpoint, snap->Type);
         isSuitableForSnapshot = true;

      } else {
         Dmsg2(DT_SNAPSHOT, "%s is not suitable for snapshot, type %s\n",
               mountpoint, snap->Type);
      }
      supportSnapshotTested = true;
   }
   return isSuitableForSnapshot;
}

/* Should be after the snapshot declaration */
fs_device *mtab::search(char *file) {
   struct stat statp;
   if (lstat(file, &statp) != 0) {
      Dmsg1(DT_SNAPSHOT, "%s not found\n", file);
      return NULL;              /* not found */
   }

   fs_device *elt = (fs_device *)entries->search((void *)((intptr_t)(statp.st_dev)),
                                                 search_entry);
   if (!elt) {
      Dmsg2(DT_SNAPSHOT, "Device %llu for file %s not found in our mount list\n",
            (uint64_t)statp.st_dev, file);
      return NULL;        /* not found in our list, skip it */
   }

   if (!elt->can_do_snapshot()) {
      Dmsg2(DT_SNAPSHOT, "Device %llu for file %s not snapshotable\n",
            (uint64_t)statp.st_dev, file);
      return NULL;
   }
   Dmsg2(DT_SNAPSHOT, "Found device %llu for file %s\n", (uint64_t)elt->dev, file);
   return elt;
}

/* Application to quiesce/un-quiesce */
struct app {
   BPIPE *fd;                   /* Communication channel */
   char  *name;                 /* Pointer to the script name */
   char   cmd[1];               /* Command line */
};

/* In the application manager, we want to run a set
 * of scripts and see if applications are running or
 * not on our partitions.
 *
 * We can specify application in the fileset, or just
 * try all application that are installed.
 *
 */
class app_manager: public SMARTALLOC
{
private:
   JCR     *jcr;
   char    *appdir;             /* Where to find application scripts */
   alist   *applst;             /* Application list (script list to call) */
   int      apptimeout;         /* Timeout when trying to quiesce application */
   mtab    *mount_list;         /* snapshot set */

public:
    app_manager(JCR *ajcr, mtab *m, char *dir): 
      jcr(ajcr),
      appdir(dir),
      applst(New(alist(10, owned_by_alist))),
      apptimeout(300),
      mount_list(m) {};

   ~app_manager() {
      delete applst;
   };

   /* Put in a file the list of all devices that
    * are in the snapshot set
    */
   bool dump_snapshotset() {
      return true;
   };

   /* Scan application */
   bool scan() {
      POOLMEM *results;
      bool  ret=false;
      char *end, *start;
      struct stat sp;
      struct app *elt = NULL;

      if (!appdir || !*appdir || stat(appdir, &sp) == -1) {
         Dmsg0(DT_SNAPSHOT, "app not configured\n");
         return true;
      }

      /* Create a file with the list of all devices that are suitable
       * for snapshot
       */
      dump_snapshotset();

      results = get_pool_memory(PM_FNAME);
      if (run_program_full_output((char*)APPMANAGER_CMD, apptimeout, results) != 0) {
         berrno be;
         Dmsg2(DT_SNAPSHOT, "app scan error results=%s ERR=%s\n", results, be.bstrerror());
         goto bail_out;
      }

      ret = true;
      start = results;

      /* Put each line of the output in our list */
      for (start = results; start && *start;) {
         end = strchr(start, '\n');
         if (end) {
            *end = 0;
            elt = (struct app *) malloc(sizeof(struct app) + strlen(start) + 1);

            elt->fd = NULL;
            strcpy(elt->cmd, start);
            elt->name = (char *)last_path_separator(elt->cmd);
            if (elt->name) {
               elt->name++;

            } else {
               elt->name = elt->cmd;
            }
 
            applst->append(elt);
            Dmsg2(10, "+ %s (%s)\n", elt->name, elt->cmd);
            *end = '\n';
            end++;
         }
         start = end;
      }
   bail_out:
      free_pool_memory(results);
      return ret;
   };

   bool unquiesce() {
      if (applst->size() == 0) {
         return true;
      }

      Jmsg(jcr, M_INFO, 0, _("Un-Quiescing applications\n"));
      return true;
   };

   /* Quiesce applications */
   bool quiesce() {
      bool ret = true;

      if (applst->size() == 0) {
         return true;
      }

      Jmsg(jcr, M_INFO, 0, _("Quiescing applications\n"));

      for (int i = 0 ; i < applst->size() ; i++) {
         struct app *elt = (struct app *) applst->get(i);
         elt->fd = open_bpipe(elt->cmd, 0, "rw");
         if (!elt->fd) {
            /* Unable to execute the program */
            continue;
         }
         /* Send some commands here */
      }
      return ret;
   };
};

snapshot_manager::snapshot_manager(JCR *ajcr):
   jcr(ajcr), mount_list(New(mtab())) {
}

snapshot_manager::~snapshot_manager() {
   delete mount_list;
}

bool snapshot_manager::cleanup_snapshots()
{
   fs_device *elt;
   foreach_rblist(elt, mount_list->entries) {
      if (elt->can_do_snapshot() && elt->inSnapshotSet) {
         snapshot *s = elt->snap;
         if (!s->created) {
            continue;
         }
         if (s->unmount()) {
            /* TODO: Display an error? Can check mounted status */
         }
         /* When no retention is set, we delete the snapshot
          * just after the backup
          */
         if (s->Retention == 0) {
            if (s->del()) {
               Jmsg(jcr, M_INFO, 0, _("   Delete Snapshot for %s\n"), elt->mountpoint);

            } else {
               Jmsg(jcr, M_ERROR, 0, _("   Unable to delete snapshot of %s ERR=%s\n"),
                    elt->mountpoint, s->errmsg);
            }
         }
      }
   }
   return true;
}

bool snapshot_manager::create_snapshots()
{
   /* No snapshot, no quiescing */
   if (mount_list->empty()) {
      Dmsg0(DT_SNAPSHOT, "The mount list is empty, no snapshot to take\n");
      return false;
   }

   /* First thing to do is to quiesce application */
   app_manager apps(jcr, mount_list, (char *)APP_DIR);

   /* TODO: Let see if we really need to abort
    * the snapshot part if application 
    */
   if (!apps.scan()) {
      return false;
   }

   if (!apps.quiesce()) {
      return false;
   }

   fs_device *elt;
   foreach_rblist(elt, mount_list->entries) {
      if (elt->can_do_snapshot() && elt->inSnapshotSet) {
         snapshot *s = elt->snap;
         if (s->create()) {
            Jmsg(jcr, M_INFO, 0, _("   Create Snapshot for %s\n"), elt->mountpoint);

            if (s->Retention > 0) {/* Create snapshot catalog entry if we need to keep them */
               s->create_catalog_entry();
            }

         } else if (s->status == 2) { /* Use Error message */
            elt->isSuitableForSnapshot = false; /* Disable snapshot for this device */
            Jmsg(jcr, M_ERROR, 0, _("   Unable to create snapshot of %s ERR=%s\n"),
                 elt->mountpoint, s->errmsg);

         } else {               /* By default, an error in the creation should be fatal */
            Jmsg(jcr, M_FATAL, 0, _("   Unable to create snapshot of %s ERR=%s\n"),
                 elt->mountpoint, s->errmsg);
            apps.unquiesce();
            return false;
         }

      } else {
         Dmsg3(DT_SNAPSHOT|20, "No Snapshot for %s suitable=%d inset=%d\n",
               elt->mountpoint, elt->isSuitableForSnapshot, elt->inSnapshotSet);
      }
   }

   /* Snapshots are ok, we need to release applications */
   if (!apps.unquiesce()) {
      return false;
   }

   /* Save the include context */
   POOL_MEM t;
   findINCEXE *old = get_incexe(jcr);
   findINCEXE *exclude = NULL;
   foreach_rblist(elt, mount_list->entries) {
      if (elt->can_do_snapshot() && elt->inSnapshotSet) {
         snapshot *s = elt->snap;

         if (!s->mount()) {
            Jmsg(jcr, M_ERROR, 0, "   Unable to mount snapshot %s ERR=%s\n",
                 elt->mountpoint, s->errmsg);

         } else if (*s->SnapDirectory) {
            if (!exclude) {
               exclude = new_exclude(jcr);
               /* Set the Exclude context */
               set_incexe(jcr, exclude);
            }
            Mmsg(t, "%s", elt->snap->SnapDirectory);
            if (add_file_to_fileset(jcr, t.c_str(), true) != state_include) {
               Jmsg(jcr, M_ERROR, 0, "   Failed to exclude=%s\n", t.c_str());
               return false;
            }
            Dmsg1(DT_SNAPSHOT|10, "Excluding %s\n", t.c_str());
         }
      }
   }

   /* Restore the current context */
   if (exclude) {
      set_incexe(jcr, old);
   }
   return true;
}

/* TODO: We might want to use some filters here */
bool snapshot_manager::list_snapshots(alist *lst)
{
   fs_device *elt;

   /* No device, no snapshot */
   if (mount_list->dCount == 0) {
      Dmsg0(DT_SNAPSHOT, "mount list is empty, no snapshot\n");
      return false;
   }

   foreach_rblist(elt, mount_list->entries) {
      if (elt->can_do_snapshot()) {
         elt->snap->list(lst);
      }
   }
   return true;
}

void snapshot_manager::add_mount_point(uint32_t dev, const char *device,
                                       const char *mountpoint, const char *fstype)
{
   bool       check=true;
   alist      list(10, not_owned_by_alist);
   fs_device *vol = New(fs_device(dev, device, mountpoint, fstype));

   /* These FS are not supposed to use snapshot */
   const char *specialmp[] = {
      "/proc/",
      "/sys/",
      NULL
   };
   /* These FS are not supposed to use snapshot */
   const char *specialfs[] = {
      "autofs",
      "binfmt_misc",
      "cgroup",
      "configfs",
      "debugfs",
      "dev",
      "devpts",
      "devtmpfs",
      "ecryptfs",
      "fuse.gvfsd-fuse",
      "fusectl",
      "fd",
      "hugetlbfs",
      "mqueue",
      "proc",
      "pstore",
      "rpc_pipefs",
      "securityfs",
      "selinuxfs",
      "sysfs",
      "systemd-1",
      "tmpfs",
      NULL
   };

   /* We skip directly /proc, /sys */
   for (int i=0; specialmp[i] ; i++) {
      if (strncasecmp(specialmp[i], mountpoint, strlen(specialmp[i])) == 0) {
         check = false;
         break;
      }
   }

   if (check) {
      for (int i=0; specialfs[i] ; i++) {
         if (strcasecmp(specialfs[i], fstype) == 0) {
            check = false;
            break;
         }
      }
   }

   if (check) {
      snapshot *snap = New(snapshot(jcr));
      snap->set_name(jcr->Job);
      vol->snap = snap;

      if (snap->scan_subvolumes(vol, &list)) {
         for (int i = list.size() - 1 ; i >= 0 ; i--) {
            fs_device *d = (fs_device *)list.remove(i);
            add_mount_point(d->dev, d->device, d->mountpoint, d->fstype);
            delete d;
         }
      }
   }

   Dmsg4(DT_SNAPSHOT|20, "Adding %s dev=%d snap?=%d to the mount list (%d)\n",
         mountpoint, dev, check, mount_list->dCount);

   if (!mount_list->add_entry(vol)) {
      Dmsg1(DT_SNAPSHOT, "%s Already exists in mount list\n", vol->mountpoint);
      delete vol;
   }
}

/* In this handler, we need to fill a mtab structure */
static void add_handler(void *user_ctx,
                        struct stat *st,
                        const char *fstype,
                        const char *mountpoint,
                        const char *mntopts,
                        const char *device)
{
   Dmsg5(DT_SNAPSHOT|50, "dev=%llu device=%s mountpoint=%s fstype=%s mntopts=%s\n",
         (int64_t)st->st_dev, device, mountpoint, fstype, mntopts);

   /* TODO: If the fstype is btrfs or zfs, the fs might contains subvolumes,
    * and these subvolumes may not be reported in the mntent list. In this
    * case, we need to call proper btrfs/zfs commands to list them.
    *   # btrfs subvolume list /mnt
    *   ID 256 gen 17 top level 5 path test
    *   ID 259 gen 18 top level 5 path test/titi
    */
   snapshot_manager *snapmgr = (snapshot_manager *)user_ctx;
   snapmgr->add_mount_point(st->st_dev, device, mountpoint, fstype);
}

bool snapshot_manager::scan_mtab()
{
   return read_mtab(add_handler, this);
}


/* Scan the fileset to select the volumes to snapshot (and subvolumes when
 * FO_MULTIFS is used) but also extend the FileSet to walk these subvolumes
 * that are not anymore in the same tree as their parent */
bool snapshot_manager::scan_fileset()
{
   if (!jcr->ff || !jcr->ff->fileset) {
      Dmsg0(DT_SNAPSHOT, "No fileset associated with JCR\n");
      return false;
   }

   findFILESET *fileset = jcr->ff->fileset;
   dlistString *node;
   int64_t      flags = 0;

   for (int i=0; i<fileset->include_list.size(); i++) {

      findFOPTS  *fo;
      findINCEXE *incexe = (findINCEXE *)fileset->include_list.get(i);

      /* look through all files */
      foreach_dlist(node, &incexe->name_list) {
         char *fname = node->c_str();
         Dmsg1(DT_SNAPSHOT|20, "Inspect %s for sub volume to mark for the snapshot\n", fname);
         if (mount_list->add_in_snapshot_set(fname, &incexe->name_list, node)) {
            /* When all volumes are selected, we can stop */
            Dmsg0(DT_SNAPSHOT, "All Volumes are marked, stopping the loop here\n");
            goto all_included;
         }
      }

      foreach_alist(fo, &incexe->opts_list) {
         flags |= fo->flags; /* We are looking for FO_MULTIFS and recurse */
      }
   }

all_included:
   /* If we allow recursion and multifs, we need to include sub volumes by hand
    * in the backup list
    */
   if (flags & FO_MULTIFS) {
      fs_device *elt, *elt2;

      foreach_rblist(elt, mount_list->entries) {
         if (!elt->inFileSet) {
            continue;
         }
         alist     *lst = New(alist(10, not_owned_by_alist));
         mount_list->get_subvolumes(elt->dev, lst, jcr->ff);
         foreach_alist(elt2, lst) {
            if (elt2->inFileSet) {
               continue;
            }

            /* TODO: See how to avoid having two entries for the same directory */
            /* Add the directory explicitly in the fileset (elt->include is a */
            /* pointer to the FileSet->include_list->name_list) */
            elt->include->insert_after(new_dlistString(elt2->mountpoint), elt->node);
            Dmsg1(DT_SNAPSHOT|20, "Add a sub volume to the FileSet: %s\n", elt2->mountpoint);

            if (mount_list->add_in_snapshot_set(elt2, elt->include, elt->node)) {
               /* When all volumes are selected, we can stop */
               Dmsg0(DT_SNAPSHOT, "All Volumes are marked, stopping the loop here\n");
               delete lst;
               return true;
            }
         }
         delete lst;
      }
   }
   return true;
}

int snapshot_cmd(JCR *jcr)
{
   BSOCK *dir = jcr->dir_bsock;
   int n, ret = 1;
   char ed1[50];
   snapshot snap(jcr);
   snap.check_buffer_size(dir->msglen);

   n = sscanf(dir->msg, QueryCmd, snap.Name, snap.Volume, snap.Device, &snap.CreateTDate, snap.Type);
   if (n == 5) {
      snap.unbash_spaces();
      Dmsg0(DT_SNAPSHOT|10, "Doing query of a snapshot\n");
      n = snap.query();
      bash_spaces(snap.errmsg);
      dir->fsend("%d Snapshot status=%d size=%lld ERR=%s\n", n?2000:2999, snap.status, snap.size, snap.errmsg);
      goto bail_out;
   }

   n = sscanf(dir->msg, LsCmd, snap.Name, snap.Volume, snap.Device, &snap.CreateTDate, snap.Type, snap.path);
   if (n == 6) {
      snap.unbash_spaces();
      Dmsg0(DT_SNAPSHOT|10, "Doing ls of a snapshot\n");
      n = snap.ls(dir);
      dir->signal(BNET_EOD);
      goto bail_out;
   }

   n = sscanf(dir->msg, DelCmd, snap.Name, snap.Volume, snap.Device, &snap.CreateTDate, snap.Type);
   if (n == 5) {
      Dmsg0(DT_SNAPSHOT|10, "Doing del of a snapshot\n");
      snap.unbash_spaces();
      n = snap.del();
      bash_spaces(snap.errmsg);
      dir->fsend("%d Snapshot deleted ERR=%s\n", n?2000:2999, snap.errmsg);
      goto bail_out;
   }

   n = sscanf(dir->msg, PruneCmd, snap.Volume, snap.Type);
   if (n == 2) {
      snap.unbash_spaces();
      n = snap.prune(dir);
      bash_spaces(snap.errmsg);
      dir->fsend("%d Snapshot pruned ERR=%s\n", n?2000:2999, snap.errmsg);
      Dmsg0(DT_SNAPSHOT|10, "Doing pruning of snapshots\n");
      goto bail_out;
   }

   n = sscanf(dir->msg, SyncCmd, snap.Volume, snap.Type);
   if (n == 2) {
      snap.unbash_spaces();
      n = snap.sync(dir);
      bash_spaces(snap.errmsg);
      dir->fsend("%d Snapshot synced ERR=%s\n", n?2000:2999, snap.errmsg);
      Dmsg0(DT_SNAPSHOT|10, "Doing sync of snapshots\n");
      goto bail_out;
   }

   /* TODO: Include a path name or a device name */
   if (strncmp(dir->msg, ListCmd, strlen(ListCmd)) == 0) {
      snapshot *elt;
      char      ed1[50];
      alist    *lst = New(alist(10, not_owned_by_alist));
      list_all_snapshots(jcr, lst);
      foreach_alist(elt, lst) {
         elt->bash_spaces();
         dir->fsend("volume=\"%s\" createtdate=\"%s\" name=\"%s\" device=\"%s\" status=%d error=\"%s\" type=\"%s\"\n",
                    elt->Volume, edit_uint64(elt->CreateTDate, ed1), 
                    elt->Name, elt->Device, elt->status, elt->errmsg, elt->Type);
         delete elt;
      }
      delete lst;
      dir->signal(BNET_EOD);      
      goto bail_out;
   }

   n = sscanf(dir->msg, ConfCmd, ed1);
   if (n == 1) {
      jcr->snapshot_retention = str_to_uint64(ed1);
      dir->fsend("2000 Snapshot retention\n");
      goto bail_out;
   }

   dir->fsend("2999 Snapshot command not found\n");
   dir->signal(BNET_EOD);
   ret = 0;

bail_out:
   return ret;
}


#ifdef HAVE_WIN32
BOOL VSSPathConvertW(const wchar_t *szFilePath, wchar_t *szShadowPath, int nBuflen);

/* This function is called by find_files(), once for every "Include" entries
 * in the FileSet.
 * This function must redirect "ff->top_fname" to its snapshot version if it
 * exists
 */
bool snapshot_convert_path(JCR *jcr, FF_PKT *ff, dlist *filelist, dlistString *node)
{
   Dmsg1(DT_SNAPSHOT, "snapshot_convert_path(%s)\n", ff->top_fname);
   // FYI jcr->snap_mgr==NULL because Win32 use VSSClient instead

   /* Convert the filename to the original path */
   /* get the path of "ff->top_fname" inside the snapshots */
   // convert to Windows wchar_t path
   POOL_MEM wpath(PM_FNAME);
   POOL_MEM wsnap(PM_FNAME);
   make_win32_path_UTF8_2_wchar(&wpath.addr(), ff->top_fname);
   wchar_t *p = (wchar_t *)wpath.c_str();
   Dmsg2(10, "ASX Convert wchart %s => %ls\n", ff->top_fname, p);
   /* ff->top_fname = "C:/" => p = "\\?\C:\" */
   DWORD len = wcslen(p);
#if 0
   // remove any trailing '/' or '\\'
      while (IsWPathSeparator(p[len-1])) {
         len--;
         p[len]=L'\0';
      }
      Dmsg2(10, "ASX Convert wchart %s => %ls\n", ff->top_fname, p);
#endif
   // get the snapshot path
   len+=MAX_PATH; // allocate enough space for "GLOBALROOT\Device\HarddiskVolumeShadowCopyXXX"
   wsnap.check_size(2*len);
   BOOL vss_exist=VSSPathConvertW((wchar_t *)&wpath.c_str()[8], (wchar_t *)wsnap.c_str(), len); // 8 == skip r"\\?\" prefix
   if (!vss_exist) {
      return true;
   }
   // Their is a snapshot for this path, I'll need some buffers
   if (!ff->snap_top_fname) {
      ff->snap_top_fname = get_pool_memory(PM_FNAME);
   }
   // convert back to utf8
   wchar_path_2_wutf8(&ff->snap_top_fname, (wchar_t *)wsnap.c_str());
   // end convert Windows '\' into posix '/'
   char *q=ff->snap_top_fname;
   while (*q) {
      if (*q=='\\') {
         *q='/';
      }
      q++;
   }
   ff->root_of_volume = false;
   // add a trailing '\' to snap_top_fname if it is the root of the snapshot volume.
   // "None" of the Windows API can deal with this kind of path without the trailing '\'
   if (strncmp(ff->snap_top_fname, "//?/GLOBALROOT/Device/HarddiskVolumeShadowCopy", 46) == 0) {
      q=&ff->snap_top_fname[46];
      while (*q!='\0' && isdigit(*q)) q++;
      if ((*q=='\0' || (*q=='/' && q[1]=='\0')) && ff->statp.st_rdev==WIN32_REPARSE_NONE) {
         ff->root_of_volume = true;
      }
      if (*q=='\0') {
         pm_strcat(ff->snap_top_fname, "/"); // of course this is posix, then use '/'
      }
   }

   // ff->volume_path && ff->snapshot_path are just pointer without any allocated
   // space, then they must point to some other pointer, snapshot::convert_path()
   // does the same
   ff->volume_path = ff->top_fname;    /* c:\tmp */
   ff->top_fname_save = ff->top_fname;

   ff->snapshot_path = ff->snap_top_fname;

   ff->top_fname = ff->snap_top_fname;

   Dmsg2(20, "ASX SnapConPath top_fname=%s snapshot_path=%s\n", ff->top_fname, ff->snapshot_path);

   return true;
}
#else
bool snapshot_convert_path(JCR *jcr, FF_PKT *ff, dlist *filelist, dlistString *node)
{
   Dmsg1(DT_SNAPSHOT, "snapshot_convert_path(%s)\n", ff->top_fname);
   snapshot_manager *snapmgr = jcr->snap_mgr;

   if (!snapmgr) {
      return true;
   }

   fs_device *elt = snapmgr->mount_list->search(ff->top_fname);
   if (!elt) {
      return true;              /* not found */
   }


   /* Convert the filename to the original path */
   if (!elt->snap->convert_path(ff)) {
      Dmsg2(DT_SNAPSHOT, "Device %llu for file %s not snapshotable\n",
            (uint64_t)elt->dev, ff->top_fname);
      return true;
   }
   return true;
}
#endif

/* ListSnap[] = "CatReq Job=%s list_snapshot name=%s volume=%s device=%s tdate=%d type=%s before=%s after=%s expired=%d"; */

/* List Catalog entry of the current client */
int snapshot_list_catalog(JCR *jcr,
                          const char *query,
                          alist *lst)
{
   int ret = 0, i;
   arg_parser cmd;
   POOL_MEM q, tmp;
   if (cmd.parse_cmd(query) != bRC_OK) {
      Dmsg1(DT_SNAPSHOT, "Unable to decode query %s\n", query);
      return 0;
   }
   Mmsg(q, "CatReq Job=%s list_snapshot name=", jcr->Job);
   if ((i = cmd.find_arg_with_value("name")) >= 0) {
      bash_spaces(cmd.argv[i]);
      pm_strcat(q, cmd.argv[i]);
   }

   pm_strcat(q, " volume=");
   if ((i = cmd.find_arg_with_value("volume")) >= 0) {
      bash_spaces(cmd.argv[i]);
      pm_strcat(q, cmd.argv[i]);
   }

   pm_strcat(q, " device=");
   if ((i = cmd.find_arg_with_value("device")) >= 0) {
      bash_spaces(cmd.argv[i]);
      pm_strcat(q, cmd.argv[i]);
   }

   pm_strcat(q, " tdate=");
   if ((i = cmd.find_arg_with_value("tdate")) >= 0) {
      bash_spaces(cmd.argv[i]);
      pm_strcat(q, cmd.argv[i]);
   }

   pm_strcat(q, " type=");
   if ((i = cmd.find_arg_with_value("type")) >= 0) {
      bash_spaces(cmd.argv[i]);
      pm_strcat(q, cmd.argv[i]);
   }

   pm_strcat(q, " before=");
   if ((i = cmd.find_arg_with_value("before")) >= 0) {
      bash_spaces(cmd.argv[i]);
      pm_strcat(q, cmd.argv[i]);
   }

   pm_strcat(q, " after=");
   if ((i = cmd.find_arg_with_value("after")) >= 0) {
      bash_spaces(cmd.argv[i]);
      pm_strcat(q, cmd.argv[i]);
   }

   pm_strcat(q, " expired=");
   if ((i = cmd.find_arg_with_value("expired")) >= 0) {
      bash_spaces(cmd.argv[i]);
      pm_strcat(q, cmd.argv[i]);
   }

   jcr->dir_bsock->fsend("%s\n", q.c_str());

   while (jcr->dir_bsock->recv() > 0) {
      if (cmd.parse_cmd(jcr->dir_bsock->msg) != bRC_OK) {
         Dmsg1(DT_SNAPSHOT, "Unable to decode director output %s\n", jcr->dir_bsock->msg);

      } else {
         ret = 1;               /* OK */
         snapshot *s = New(snapshot(jcr));
         s->scan_arg(&cmd);
         lst->append(s);
      }
   }
   return ret;
}
